﻿using CRMField.Win8App.Common;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using Windows.Networking.Connectivity;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using System.IO;
using CrmField.Win8App.Common;
using Microsoft.Crm.Sdk.Messages;

namespace CrmField.Win8App.Data
{
    /// <summary>
    /// Service Appointment Data Source
    /// 
    /// This class 
    /// </summary>
    [DataContract]
    public class ActivityDataSource : BindableBase
    {
        #region Singleton Static Members
        /// <summary>
        /// Provides the Curent Data Source if it has been initialised
        /// </summary>
        [IgnoreDataMember]
        public static ActivityDataSource CurrentDataSource
        {
            private set;
            get;
        }

        /// <summary>
        /// Initialises the current data source. Firs trying to load it from a file, and then from the server.
        /// </summary>
        /// <param name="fileName">Name of the DataSource storage file</param>
        /// <returns></returns>
        public static async Task LoadFromFileOrService(String fileName)
        {
            ActivityDataSource ds = null;

            // First try to load from File Storage.
            StorageFile file = null;
            try
            {
                file = await ApplicationData.Current.LocalFolder.GetFileAsync(fileName);
            }
            catch (FileNotFoundException)
            {
                // Do Nothing
            }

            if (file != null)
            {
                //Deserialise the contents
                try
                {
                    using (IInputStream stream = await file.OpenSequentialReadAsync())
                    {
                        DataContractSerializer serializer = new DataContractSerializer(typeof(ActivityDataSource), new DataContractSerializerSettings() { PreserveObjectReferences = true });
                        ds = (ActivityDataSource)serializer.ReadObject(stream.AsStreamForRead());
                    }
                }
                catch (Exception e)
                {
                    throw new ActivityDataSourceException("File Load Error", e);
                }
            }
            else
            {
                try
                {
                    ds = new ActivityDataSource(fileName);
                    //TODO Decouple CRM Context
                    var service = await CrmContextManager.GetConnectionAsync();
                    await ds.LoadFromService(service);
                }
                catch (Exception e)
                {
                    throw new ActivityDataSourceException("Could not retrieve from service", e);
                }

                ds.SaveToFile(); // we don't await for this one.
            }


            ActivityDataSource.CurrentDataSource = ds;
        }
        #endregion

        [DataMember]
        public string LocalFileName
        {
            private set;
            get;
        }

        private ObservableCollection<ActivityDataGroup> _allGroups;
        [DataMember]
        public ObservableCollection<ActivityDataGroup> AllGroups
        {
            get
            {
                if (this._allGroups == null)
                {
                    this._allGroups = new ObservableCollection<ActivityDataGroup>();
                }
                return this._allGroups;
            }
        }

        private DateTime _lastSync;
        /// <summary>
        /// Date and Time of Last Sync with the Server
        /// </summary>
        [DataMember]
        public DateTime LastSync
        {
            get { return _lastSync; }
            set
            {
                this.SetProperty(ref this._lastSync, value);
            }
        }



        public IEnumerable<ActivityDataGroup> Groups
        {
            get
            {
                return this.AllGroups;
            }
        }

        public ActivityDataGroup GetGroup(DateTime uniqueId)
        {
            // Simple linear search is acceptable for small data sets
            var matches = this.AllGroups.Where((group) => group.Date.Equals(uniqueId));
            if (matches.Count() == 1) return matches.First();
            return null;
        }

        public ServiceAppointment GetItem(Guid uniqueId)
        {
            // Simple linear search is acceptable for small data sets
            var matches = this.AllGroups.SelectMany(group => group.Items).Where((item) => item.ActivityId.Equals(uniqueId));
            if (matches.Count() == 1) return matches.First();
            return null;
        }



        private ActivityDataSource()
        { }

        private ActivityDataSource(string fileName) :
            this()
        {
            this.LocalFileName = fileName;
        }

        private ActivityDataSource(IEnumerable<ServiceAppointment> appointments, string fileName) :
            this(fileName)
        {
            this.Initialise(appointments);
        }


        private async Task LoadFromService(OrganizationServiceProxy orgService)
        {
            var fetchQuery = new FetchExpression(
                @"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='true'>
                                    <entity name='serviceappointment'>
                                    <attribute name='subject' />
                                    <attribute name='customers' />
                                    <attribute name='serviceid' />
                                    <attribute name='scheduledstart' />
                                    <attribute name='activityid' />
                                    <attribute name='location' />
                                    <order attribute='scheduledstart' descending='false' />
                                    <filter type='and'>
                                        <condition attribute='statecode' operator='in'>
                                        <value>0</value>
                                        <value>3</value>
                                        </condition>
                                    </filter>
                                    <link-entity name='activityparty' from='activityid' to='activityid' alias='ab'>
                                        <filter type='and'>
                                        <condition attribute='partyid' operator='eq-userid' />
                                        <condition attribute='participationtypemask' operator='in'>
                                            <value>10</value>
                                            <value>9</value>
                                        </condition>
                                        </filter>
                                    </link-entity>
                                    </entity>
                                </fetch>");

            var results = await orgService.RetrieveMultipleAsync<ServiceAppointment>(fetchQuery);

            this.Initialise(results);

            this.LastSync = DateTime.Now;
        }

        private void Initialise(IEnumerable<ServiceAppointment> appointments)
        {
            this.AllGroups.Clear();

            foreach (var item in appointments)
            {
                if (!item.ScheduledStart.HasValue)
                {
                    item.ScheduledStart = DateTime.Now;
                }

                AddItemToGroup(item);
            }
        }

        private void AddItemToGroup(ServiceAppointment item)
        {
            if (item.Group != null)
            {
                item.Group.Items.Remove(item);
            }

            //Check if date is already there
            var group = GetGroup(item.ScheduledStart.Value.Date);

            if (group == null)
            {
                group = new ActivityDataGroup(item.ScheduledStart.Value.Date);
                this.AllGroups.Add(group);
            }
            item.Group = group;
            group.Items.Add(item);
        }

        public async Task SyncWithService()
        {
            var orgService = await CrmContextManager.GetConnectionAsync();
            var dirtyItems = this.AllGroups.SelectMany(group => group.Items).Where((item) => item.IsDirty == true);

            //1. TODO: Sent Changes
            foreach (var item in dirtyItems)
            {
                if (item.StatusCode.Value == 8)
                {
                    //Mark as complete
                    await MarkCompleteOnService(item);
                }
                else
                {
                    //Data changed
                    await ReScheduleOnService(item);
                }

            }


            //2. ReLoad
            await this.LoadFromService(orgService);

            await this.SaveToFile();
        }

        private async Task LoadFromService(String fileName)
        {
            //TODO: Improve CRM Context
            var ds = new ActivityDataSource(fileName);
            var service = await CrmContextManager.GetConnectionAsync();
            await ds.LoadFromService(service);

            ActivityDataSource.CurrentDataSource = ds;
        }

        public async Task SaveToFile()
        {
            try
            {
                // Serialize the session state synchronously to avoid asynchronous access to shared
                // state
                MemoryStream sessionData = new MemoryStream();
                DataContractSerializer serializer = new DataContractSerializer(typeof(ActivityDataSource), new DataContractSerializerSettings() { PreserveObjectReferences = true });
                serializer.WriteObject(sessionData, this);

                // Get an output stream for the SessionState file and write the state asynchronously
                StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync(this.LocalFileName, CreationCollisionOption.ReplaceExisting);
                using (Stream fileStream = await file.OpenStreamForWriteAsync())
                {
                    sessionData.Seek(0, SeekOrigin.Begin);
                    await sessionData.CopyToAsync(fileStream);
                    await fileStream.FlushAsync();
                }

            }
            catch (Exception e)
            {
                throw new ActivityDataSourceException("Failed to Save File", e);
            }
        }

        #region Actions on Appointments

        public void MarkComplete(ServiceAppointment appointment)
        {
            appointment.StatusCode = new OptionSetValue(8);
            appointment.IsDirty = true;
        }


        public void ReSchedule(ServiceAppointment appointment, DateTime newdate)
        {
            appointment.ScheduledStart = newdate;
            appointment.IsDirty = true;
            AddItemToGroup(appointment);
        }

        private async Task MarkCompleteOnService(ServiceAppointment appointment)
        {
            if (appointment.ActivityId != null)
            {
                var service = await CrmContextManager.GetConnectionAsync();
                SetStateRequest setStateRequest = new SetStateRequest();
                setStateRequest.EntityMoniker = new EntityReference(ServiceAppointment.EntityLogicalName, appointment.ActivityId.Value);
                setStateRequest.State = new OptionSetValue((int)ServiceAppointmentState.Closed);
                setStateRequest.Status = new OptionSetValue(8); // C
                var setStateResponse = await service.ExecuteAsync<SetStateRequest, SetStateResponse>(setStateRequest);
            }
        }

        private async Task ReScheduleOnService(ServiceAppointment appointment)
        {
            if (appointment.ActivityId != null)
            {
                var service = await CrmContextManager.GetConnectionAsync();
                RescheduleRequest rescheduelRequest = new RescheduleRequest();
                rescheduelRequest.Target = appointment;
                var rescheduleResponse = await service.ExecuteAsync<RescheduleRequest, RescheduleResponse>(rescheduelRequest);
            }
        }

        #endregion

    }

    public class ActivityDataSourceException : Exception
    {
        public ActivityDataSourceException()
        {
        }

        public ActivityDataSourceException(Exception e)
            : base("ActivityDataSourceException failed", e)
        {

        }

        public ActivityDataSourceException(string message, Exception e)
            : base(message, e)
        {

        }
    }



}
